<?php

/**
 * ExtASTWalker class file
 *
 * @author TrueSoftware B.V. (The Netherlands)
 * @link http://www.ext4yii.com
 * @copyright Copyright 2007-2010 TrueSoftwareB.V. all rights reserved.
 * @license http://www.ext4yii.com/license/
 *          EVALUATION VERSION - NOT FOR COMMERCIAL USE OR DISTRIBUTION.
 * @version 0.7.2
 */

define('HDOM_TYPE_EXTCOMP', 100);
define('HDOM_TYPE_EXTPROP', 101);

class ExtASTWalker
{
    public $srcdom;
    public $objects = array();
    public $options = array();
    private $script_manager = null;
    private $code_templates = array();
    private $controller;

    public function getController()
    {
        return $this->controller;
    }

    public function getOption($name, $default)
    {
        if (isset($this->options[$name]))
            return $this->options[$name];
        else
            return $default;
    }

    public function removeOption($name)
    {
        if (isset($this->options[$name]))
            unset ($this->options[$name]);
    }

    public function renderPlugins($plugins)
    {
        $result = array();
        foreach ($plugins as $key => $item)
            $result[] = sprintf('%s = %s', $key, $item->renderConfig(true));
        return $result;
    }

    /**
     * Internal function for rendering data stores
     * @param ExtSTore $stores
     * @return array
     */
    public function renderStores($stores)
    {
        $result = array();
        foreach ($stores as $key => $item)
            $result[] = sprintf('var %s=%s', $key, $item->renderConfig(true));
        return $result;
    }

    /**
     * @internal method.
     * This method is used to pass component properties from parent to child.
     * It is single level passing and does not propagate to parent or child components
     * using the ASTWalker options mechanism
     * @param ExtWidget $extctl
     * @param array $values
     */
    private function setPassedProperties($extctl)
    {
        if (isset($this->options['passedProperties']))
        {
            $props = $this->options['passedProperties'];
            if (!is_null($extctl) && is_array($props))
            {
                foreach ($props as $prop => $value)
                    $extctl->$prop = $value;
            }
        }
    }

    public function walk($root=null, $parent=null, $allowedTypes=null)
    {
        $check_unparsed = false;

        if ($root == null)
        {
            $root = $this->srcdom->root;
            $check_unparsed = true;
        }

        foreach ($root->children as $node)
        {
            if ($node->nodetype == HDOM_TYPE_EXTCOMP)
            {
                $extctl = $node->extctl;
                $this->setPassedProperties($extctl);


                /* if this node is in a prop:* element then set the element type */
                $node->virtualParent = $this->getOption('virtualParent', null);

                /*
                 * Check the allowedTag types
                 */
                if (!is_null($allowedTypes))
                {
                    $is_direct_type = array_search(get_class($extctl), $allowedTypes) === false ? false : true;
                    $is_instanceof = false;
                    foreach ($allowedTypes as $type)
                    {
                        if (is_subclass_of($extctl, $type))
                        {
                            $is_instanceof = true;
                            break;
                        }
                    }

                    if (!$is_direct_type && !$is_instanceof)
                        throw new Exception(Ext4Yii::t('ext4yii', "In {class} the {exttag} only accepts one of (or an instanceof) {tags}.\nAn {inst} was provided!",
                                        array(
                                            '{class}' => get_class($parent),
                                            '{exttag}' => $root->exttag,
                                            '{tags}' => implode(' or ', $allowedTypes),
                                            '{inst}' => get_class($extctl)
                                )));
                }

                $extctl->setController($this->getController());
                $extctl->setAstWalker($this);
                $extctl->parent = $parent;
                $extctl->init();
                $extctl->parseNode($node, $this);
                $extctl->run();
                $extctl->createContainer($node);
                $extctl->parsed = true;

                $appendedConfigs = $this->getOption('appendedConfigs', array());
                if (count($appendedConfigs) != 0)
                    $extctl->mergeConfigs($appendedConfigs);

                $appendedMappings = $this->getOption('appendedMappings', array());
                if (count($appendedMappings) != 0)
                    $extctl->mergeMappings($appendedMappings);

                if ($extctl instanceof ExtStore)
                {
                    $this->getController()->addDataStore($extctl);
                }
                elseif ($extctl instanceof ExtPlugin)
                {
                    $this->getController()->addPlugin($extctl);
                }
                else
                {
                    $config = $extctl->renderConfig($this->getOption('asobject', true));
                    if (!is_null($config))
                        $this->objects[] = $config;
                }

                foreach ($node->children as $item)
                {
                    $propcheck = ($item->nodetype == HDOM_TYPE_EXTPROP && !$item->walked && !$extctl->supportsProperty($item->tag));
                    $compcheck = ($item->nodetype == HDOM_TYPE_EXTCOMP);
                    if ($propcheck)
                    {
                        throw new Exception(
                                Ext4Yii::t('ext4yii', "Unable to continue!. The {comp} component does not support the {prop} tag.",
                                        array('{comp}' => $node->tag, '{prop}' => $item->tag))
                        );
                    }
                    else if ($compcheck)
                    {
                        throw new Exception(
                                Ext4Yii::t('ext4yii', "Unable to continue!. The {comp} component is nested outside any supported property tag within the {host}",
                                        array('{comp}' => $item->tag, '{host}' => str_replace('Ext', 'ext:', get_class($extctl))))
                        );
                    }
                }
            }
            else if ($node->nodetype == HDOM_TYPE_EXTPROP)
                throw new Exception(
                        Ext4Yii::t('ext4yii', "Orphan property template {prop}. This property cannot be parsed because it is not nested in any component.\n\n{source}",
                                array('{prop}' => $node->tag, '{source}' => $node->outertext())
                        )
                );
            else
                $this->walk($node, $parent);
        }
        $root->walked = true;
    }

    public function assignAttributes(ExtHTMLParserNode $node, $object=null)
    {
        foreach ($node->attr as $attname => $attvalue)
        {
            try
            {
                if($attname == 'controller')
                    throw new Exception('!');
                
                if($attname == 'xtype' || $attname == 'xclass')
                    $object->differed_type = true;

                if ($attname == 'tag')
                    $object->tag = $attvalue;
                else if ($attname != 'classname')
                {
                    /* when ExtElement only set the id other attributes will
                     * will stay on the element
                     */
                    if ($object instanceof ExtElement)
                    {
                        if ($attname == 'type')
                            call_user_func(array($object, 'set' . ucfirst($attname)), $attvalue);
                    }
                    else
                    {
                        call_user_func(array($object, 'set' . ucfirst($attname)), $attvalue);
                    }
                }
            } catch (Exception $ex)
            {
                throw new Exception(Ext4Yii::t('ext4yii', 'Unknown attribute {attr} for {class}', array(
                            '{attr}' => $attname,
                            '{class}' => get_class($object))));
            }
        }
    }

    private function createExtControl(ExtHTMLParserNode $node)
    {
        $classname = $this->createExtClassName($node);
        $class_ok = (YII_DEBUG ? class_exists($classname, true) : @class_exists($classname, true));
        if (!$class_ok)
        {
            throw new CException(Ext4Yii::t('ext4yii', 'Unknown Ext4Yii component: {name} {tag}',
                            array('{name}' => $node->tag, '{tag}' => $node->text)));
        }
        else
        {
            $ctl = new $classname();
            $this->assignAttributes($node, $ctl);
            return $ctl;
        }
    }

    public function getScriptManager()
    {
        return $this->script_manager;
    }

    private function createExtClassName($node)
    {
        return 'Ext' . $node->attr['classname'];
    }

    public function getDocument()
    {
        $doc = $this->srcdom->save();
        foreach ($this->code_templates as $key => $value)
            $doc = str_replace($key, $value, $doc);
        return $doc;
    }

    function assignExtNodeTypes($dom)
    {
        foreach ($dom->nodes as $node)
        {
            $tst = explode(':', strtolower($node->tag));
            if (is_array($tst) && count($tst) == 2)
            {
                switch ($tst[0])
                {
                    case 'ext':
                        $node->nodetype = HDOM_TYPE_EXTCOMP;
                        $node->exttag = $tst[1];
                        break;
                    case 'prop':
                        $node->nodetype = HDOM_TYPE_EXTPROP;
                        $node->exttag = $tst[1];
                        break;
                }
                ;
            }
        }
    }

    public function __construct($source, $options, $controller)
    {
        $this->options = $options;
        $this->controller = $controller;

        if (!is_string($source))
        {
            if ($source instanceof ExtHTMLParserNode)
            {
                $this->srcdom = $source->dom;
            }
        }
        else
        {
            $this->prepareDom($source);
            $this->instantiateControls();
        }
    }

    private function instantiateControls()
    {
        foreach ($this->srcdom->nodes as $node)
        {
            if ($node->nodetype == HDOM_TYPE_EXTCOMP)
            {
                $node->extctl = $this->createExtControl($node);
                if ($node->extctl instanceof ExtScriptManager)
                    $this->script_manager = $node->extctl;
            }
        }
    }

    private function codeTemplateParser($source)
    {
        $dom = new ExtHTMLParserDom();
        $dom->load($source, false);
        $this->cleanDocument($dom, true);

        foreach ($dom->nodes as $node)
        {
            if (strtolower($node->tag) == 'ext:code')
            {
                $key = '{' . md5($node->innertext) . '}';
                $this->code_templates[$key] = htmlentities($node->innertext);
                $node->innertext = $key;
            }
        }
        return $dom->save();
    }

    private function classNameParser($dom)
    {
        foreach ($dom->nodes as $node)
        {
            if (stristr($node->tag, 'ext:') !== false)
                $node->attr[' classname'] = str_replace(':', '', stristr($node->tag, ':'));
        }
    }

    private function cleanDocument($dom, $skip_comments=false)
    {
        $idx = 0;
        foreach ($dom->nodes as $node)
        {
            if ($node->nodetype == HDOM_TYPE_COMMENT && !$skip_comments)
                $node->outertext = '';
            if ($node->nodetype == HDOM_TYPE_DOCTYPE && $idx > 1)
                $node->outertext = '';
            if ($node->nodetype == HDOM_TYPE_ELEMENT && strtolower($node->tag) == 'ext4yii')
                $node->outertext = '';
            $idx++;
        }
        return false;
    }

    private function prepareDom($source)
    {
        /*
         * The order of the parsers must be preserved
         */
        $parsers = array(
            'cleanDocument'
        );

        $source = $this->codeTemplateParser($source);

        /*
         * Classnames are case sesitive in unix environments.
         * Because the dom parser forces the tag names to lower case,
         * we have to preserve the original tag casing provided by the user
         * and then remove this later.
         */
        $dom = new ExtHTMLParserDom();
        $dom->load($source);
        $this->classNameParser($dom);
        $source = $dom->save();

        for ($idx = 0; $idx != count($parsers); $idx++)
        {
            $parser = $parsers[$idx];
            $dom = new ExtHTMLParserDom();
            $dom->load($source, true);
            $this->assignExtNodeTypes($dom);

            if (call_user_func(array($this, $parser), $dom) === true)
                $idx--;

            $source = $dom->save();
            $dom = null;
        }

        $this->srcdom = new ExtHTMLParserDom();
        $this->srcdom->load($source);
        $this->assignExtNodeTypes($this->srcdom);
    }

}

?>
